﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.DynamicsProgressMark"/> to  Windows Presentation Foundation view.
    /// </summary>
    public class DynamicsProgressMark : Attachment, IPositionable
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DynamicsProgressMark"/> class.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The staff that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="DynamicsProgressMark"/>
        /// </para>
        /// <para>
        /// The adapted element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        public DynamicsProgressMark(Staff owner, Model.Elements.CWNotation.DynamicsProgressMark adaptedElement)
            : base(owner, adaptedElement)
        {
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override string ElementNameKey
        {
            get { return elementNameKey; }
        }

        #endregion

        #region IPositionable Members

        /// <inheritdoc/>
        public double Left
        {
            get 
            {
                return (double)LengthConverter.Convert(
                    GetPropertyValue<Model.Elements.Point>("Position").X, typeof(double), null, CultureInfo.CurrentCulture
                    );
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Position"))
                {
                    var cmd = (SetterCommand<Model.Elements.Point>)UnsavedChanges["Position"];
                    cmd.Value = (Model.Elements.Point)StructuresConverter.ConvertBack(
                        new Point(value, Top), typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture
                        );
                }
                else
                    UnsavedChanges["Position"] = CreateSetPositionCommand(new Point(value, Top));

                RaisePropertyChanged("Left");
                RaisePropertyChanged("Position");
            }
        }

        /// <inheritdoc/>
        public double Top
        {
            get
            {
                return (double)LengthConverter.Convert(
                    GetPropertyValue<Model.Elements.Point>("Position").Y, typeof(double), null, CultureInfo.CurrentCulture
                    );
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Position"))
                {
                    var cmd = (SetterCommand<Model.Elements.Point>)UnsavedChanges["Position"];
                    cmd.Value = (Model.Elements.Point)StructuresConverter.ConvertBack(
                        new Point(Left, value), typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture
                        );
                }
                else
                    UnsavedChanges["Position"] = CreateSetPositionCommand(new Point(Left, value));

                RaisePropertyChanged("Top");
                RaisePropertyChanged("Position");
            }
        }

        #endregion

        #region Event Handlers

        /// <inheritdoc/>
        protected override bool AdaptedElement_PropertyChanged(PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "ProgressType" || e.PropertyName == "Visualization" || e.PropertyName == "Abbreviated")
                RaisePropertyChanged("Text");

            return base.AdaptedElement_PropertyChanged(e);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Abbreviated"/> property.
        /// </summary>
        /// <param name="abbreviated">
        /// <para>
        /// Type: <see cref="bool"/>
        /// </para>
        /// <para>
        /// New value of the property.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetAbbreviatedCommand(bool abbreviated)
        {
            return new SetterCommand<bool>(this, "Abbreviated", abbreviated, UndoStack, RedoStack)
            {
                Name = "Toggle Abbreviated"
            };
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Expression"/> property.
        /// </summary>
        /// <param name="expression">
        /// <para>
        /// Type: <see cref="String"/>
        /// </para>
        /// <para>
        /// The expression.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetExpressionCommand(string expression)
        {
            return new SetterCommand<string>(this, "Expression", expression, UndoStack, RedoStack)
            {
                Name = "Change Expression"
            };
        }

        /// <summary>
        /// Creates an undoable command for setting a hairpin control point.
        /// </summary>
        /// <param name="propertyName">
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// Name of the property that identifies the control point.
        /// Must be either <see cref="HairpinEnd1"/>, <see cref="HairpinEnd2"/> or <see cref="HairpinPoint"/>.
        /// </para>
        /// </param>
        /// <param name="point">
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The new coordinates of the point.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentException">
        /// The <paramref name="propertyName"/> is not valid.
        /// </exception>
        public UndoableCommandBase CreateSetHairpinControlPointCommand(string propertyName, Point point)
        {
            if (propertyName != "HairpinEnd1" && propertyName != "HairpinEnd2" && propertyName != "HairpinPoint")
                throw new ArgumentException("Invalid property name; must be one of HairpinEnd1, HairpinEnd2 or HairpinPoint.", "propertyName");

            var modelPoint = (Model.Elements.Point)StructuresConverter.ConvertBack(point, typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture);

            return new SetterCommand<Model.Elements.Point>(this, propertyName, modelPoint, UndoStack, RedoStack)
            {
                Name = "Modify Hairpin"
            };
        }

        /// <summary>
        /// Creates an undoable command for setting the position of this mark.
        /// </summary>
        /// <param name="position">
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The position.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetPositionCommand(Point position)
        {
            var modelPosition = (Model.Elements.Point)StructuresConverter.ConvertBack(
                position, typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture
                );

            return new SetterCommand<Model.Elements.Point>(this, "Position", modelPosition, UndoStack, RedoStack)
            {
                Name = "Change Position"
            };
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="ProgressType"/> property.
        /// </summary>
        /// <param name="type">
        /// <para>
        /// Type: <see cref="DynamicsProgressType"/>
        /// </para>
        /// <para>
        /// The new type.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetProgressTypeCommand(DynamicsProgressType type)
        {
            var modelType = (Model.Elements.DynamicsProgressType)Enum.Parse(typeof(Model.Elements.DynamicsProgressType), type.ToString());

            return new SetterCommand<Model.Elements.DynamicsProgressType>(this, "ProgressType", modelType, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Visualization"/> property.
        /// </summary>
        /// <param name="visualization">
        /// <para>
        /// Type: <see cref="DynamicsProgressDisplayMode"/>
        /// </para>
        /// <para>
        /// The visualization.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetVisualizationCommand(DynamicsProgressDisplayMode visualization)
        {
            var modelVisualization = (Model.Elements.CWNotation.DynamicsProgressDisplayMode)Enum.Parse(
                typeof(Model.Elements.CWNotation.DynamicsProgressDisplayMode),
                visualization.ToString()
                );

            return new SetterCommand<Model.Elements.CWNotation.DynamicsProgressDisplayMode>(this, "Visualization", modelVisualization, UndoStack, RedoStack)
            {
                Name = "Change Visualization"
            };
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the value indicating whether the <see cref="Text"/> should be abbreviated or not.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// Indicates whether the <see cref="Text"/> representation of this mark
        /// should be abbreviated or not.
        /// </para>       
        /// </value>
        public bool Abbreviated
        {
            get { return GetPropertyValue<bool>("Abbreviated"); }
            set
            {
                if (UnsavedChanges.ContainsKey("Abbreviated"))
                {
                    var cmd = (SetterCommand<bool>)UnsavedChanges["Abbreviated"];
                    cmd.Value = value;
                }
                else
                    UnsavedChanges["Abbreviated"] = CreateSetAbbreviatedCommand(value);

                RaisePropertyChanged("Abbreviated");
            }
        }

        /// <summary>
        /// Gets or sets the textual expression that precises the dynamics progress.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// A textual expression that precises the dynamics progress.
        /// </para>
        /// </value>
        public string Expression
        {
            get { return GetPropertyValue<string>("Expression"); }
            set
            {
                if (UnsavedChanges.ContainsKey("Expression"))
                {
                    var cmd = (SetterCommand<string>)UnsavedChanges["Expression"];
                    cmd.Value = value;
                }
                else
                    UnsavedChanges["Expression"] = CreateSetExpressionCommand(value);

                RaisePropertyChanged("Expression");
            }
        }

        /// <summary>
        /// Gets or sets the position of the hairpin's upper end point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The coordinates (in device independent pixels) of the hairpin's upper end point
        /// relative to the top-left corner of the owner section.
        /// </para>
        /// </value>
        public Point HairpinEnd1
        {
            get 
            {
                return (Point)StructuresConverter.Convert(
                    GetPropertyValue<Model.Elements.Point>("HairpinEnd1"), typeof(Point), null, CultureInfo.CurrentCulture
                    );
            }
            set
            {
                if (UnsavedChanges.ContainsKey("HairpinEnd1"))
                {
                    var cmd = (SetterCommand<Model.Elements.Point>)UnsavedChanges["HairpinEnd1"];
                    cmd.Value = (Model.Elements.Point)StructuresConverter.ConvertBack(
                        value, typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture
                        );
                }
                else
                    UnsavedChanges["HairpinEnd1"] = CreateSetHairpinControlPointCommand("HairpinEnd1", value);

                RaisePropertyChanged("HairpinEnd1");
            }
        }

        /// <summary>
        /// Gets or sets the position of the hairpin's lower end point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The coordinates (in device independent pixels) of the hairpin's lower end point
        /// relative to the top-left corner of the owner section.
        /// </para>
        /// </value>
        public Point HairpinEnd2
        {
            get
            {
                return (Point)StructuresConverter.Convert(
                    GetPropertyValue<Model.Elements.Point>("HairpinEnd2"), typeof(Point), null, CultureInfo.CurrentCulture
                    );
            }
            set
            {
                if (UnsavedChanges.ContainsKey("HairpinEnd2"))
                {
                    var cmd = (SetterCommand<Model.Elements.Point>)UnsavedChanges["HairpinEnd2"];
                    cmd.Value = (Model.Elements.Point)StructuresConverter.ConvertBack(
                        value, typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture
                        );
                }
                else
                    UnsavedChanges["HairpinEnd2"] = CreateSetHairpinControlPointCommand("HairpinEnd2", value);

                RaisePropertyChanged("HairpinEnd2");
            }
        }

        /// <summary>
        /// Gets or sets the position of the hairpin's point.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The coordinates (in device independent pixels) of the hairpin's point
        /// relative to the top-left corner of the owner section.
        /// </para>
        /// </value>
        public Point HairpinPoint
        {
            get
            {
                return (Point)StructuresConverter.Convert(
                    GetPropertyValue<Model.Elements.Point>("HairpinPoint"), typeof(Point), null, CultureInfo.CurrentCulture
                    );
            }
            set
            {
                if (UnsavedChanges.ContainsKey("HairpinPoint"))
                {
                    var cmd = (SetterCommand<Model.Elements.Point>)UnsavedChanges["HairpinPoint"];
                    cmd.Value = (Model.Elements.Point)StructuresConverter.ConvertBack(
                        value, typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture
                        );
                }
                else
                    UnsavedChanges["HairpinPoint"] = CreateSetHairpinControlPointCommand("HairpinPoint", value);

                RaisePropertyChanged("HairpinPoint");
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is currently selected.
        /// </summary>
        /// <value>
        /// 	<see langword="true"/> if this instance is selected; otherwise, <see langword="false"/>.
        /// </value>
        public bool IsSelected
        {
            get { return isSelected; }
            set
            {
                isSelected = value;

                RaisePropertyChanged("IsSelected");
            }
        }

        /// <summary>
        /// Gets or sets the position of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The coordinates (in device independent pixels) of the top-left corner
        /// of the symbol relative to the top-left corner of the owner section.
        /// </para>
        /// </value>
        public Point Position
        {
            get { return (Point)StructuresConverter.Convert(GetPropertyValue<Model.Elements.Point>("Position"), typeof(Point), null, CultureInfo.CurrentCulture); }
            set
            {
                if (UnsavedChanges.ContainsKey("Position"))
                {
                    var cmd = (SetterCommand<Model.Elements.Point>)UnsavedChanges["Position"];
                    cmd.Value = (Model.Elements.Point)StructuresConverter.ConvertBack(value, typeof(Model.Elements.Point), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Position"] = CreateSetPositionCommand(value);

                RaisePropertyChanged("Position");
                RaisePropertyChanged("Left");
                RaisePropertyChanged("Top");
            }
        }

        /// <summary>
        /// Gets or sets the type of the dynamics progress.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="DynamicsProgressType"/>
        /// </para>
        /// <para>
        /// The type of the dynamics progress.
        /// </para>
        /// </value>
        public DynamicsProgressType ProgressType
        {
            get { return (DynamicsProgressType)Enum.Parse(typeof(DynamicsProgressType), GetPropertyValue<Model.Elements.DynamicsProgressType>("ProgressType").ToString()); }
            set
            {
                if (UnsavedChanges.ContainsKey("ProgressType"))
                {
                    var cmd = (SetterCommand<Model.Elements.DynamicsProgressType>)UnsavedChanges["ProgressType"];
                    cmd.Value = (Model.Elements.DynamicsProgressType)Enum.Parse(typeof(Model.Elements.DynamicsProgressType), value.ToString());
                }
                else
                    UnsavedChanges["ProgressType"] = CreateSetProgressTypeCommand(value);

                RaisePropertyChanged("ProgressType");
            }
        }

        /// <summary>
        /// Gets the text that represents the mark.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// The textual representation of the mark.
        /// This value only has meaning if the <see cref="Model.Elements.CWNotation.DynamicsProgressMark.Visualization"/>
        /// of the adapted mark is <see cref="Model.Elements.CWNotation.DynamicsProgressDisplayMode.Text"/>.
        /// Otherwise it is an empty string ("").
        /// </para>
        /// </value>
        public string Text
        {
            get 
            {
                if (((Model.Elements.CWNotation.DynamicsProgressMark)AdaptedElement).Visualization == Model.Elements.CWNotation.DynamicsProgressDisplayMode.Text)
                {
                    switch (((Model.Elements.CWNotation.DynamicsProgressMark)AdaptedElement).ProgressType)
                    {
                        case Model.Elements.DynamicsProgressType.Crescendo:
                            return (Abbreviated ? "cresc." : "crescendo");
                        case Model.Elements.DynamicsProgressType.Decrescendo:
                            return (Abbreviated ? "decresc." : "decrescendo");
                        case Model.Elements.DynamicsProgressType.Diminuendo:
                            return (Abbreviated ? "dim." : "diminuendo");
                    }
                }

                return "";
            }
        }

        /// <summary>
        /// Gets or sets the visualization mode of this mark.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="DynamicsProgressDisplayMode"/>
        /// </para>
        /// <para>
        /// The type of the visualization of this mark.
        /// </para>
        /// </value>
        public DynamicsProgressDisplayMode Visualization
        {
            get 
            {
                return (DynamicsProgressDisplayMode)Enum.Parse(
                    typeof(DynamicsProgressDisplayMode), 
                    GetPropertyValue<Model.Elements.CWNotation.DynamicsProgressDisplayMode>("Visualization").ToString()
                    );
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Visualization"))
                {
                    var cmd = (SetterCommand<Model.Elements.CWNotation.DynamicsProgressDisplayMode>)UnsavedChanges["Visualization"];
                    cmd.Value = (Model.Elements.CWNotation.DynamicsProgressDisplayMode)Enum.Parse(typeof(Model.Elements.CWNotation.DynamicsProgressDisplayMode), value.ToString());
                }
                else
                    UnsavedChanges["Visualization"] = CreateSetVisualizationCommand(value);

                RaisePropertyChanged("Visualization");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds a dictionary key of a user-friendly name of this element.
        /// </summary>
        private const string elementNameKey = "ViewModel.ElementNames.DynamicsProgressMark";
        /// <summary>
        /// Holds the value of the <see cref="IsSelected"/> property.
        /// </summary>
        private bool isSelected;

        #endregion
    }

    /// <summary>
    /// Determines the type of visualization used for a <see cref="DynamicsProgressMark"/>.
    /// </summary>
    public enum DynamicsProgressDisplayMode
    {
        /// <summary>
        /// The mark is visualized as a hairpin (wedge).
        /// </summary>
        Hairpin,
        /// <summary>
        /// The mark is visualized using a text expression.
        /// </summary>
        Text
    }

    /// <summary>
    /// Decribes the type of the dynamics change.
    /// </summary>
    public enum DynamicsProgressType
    {
        /// <summary>
        /// Crescendo (get gradually louder).
        /// </summary>
        Crescendo,
        /// <summary>
        /// Decrescendo (get gradually softer).
        /// </summary>
        Decrescendo,
        /// <summary>
        /// Similar to <see cref="Decrescendo"/>, but usually includes
        /// a small rallentando.
        /// </summary>
        Diminuendo
    }
}
